using Godot;
using Godot.Collections;
using VexelDict = Godot.Collections.Dictionary<Godot.Vector3I, uint>;
using System.Threading;
using System.Linq;

public partial class Chunk : StaticBody3D
{
    const int CHUNK_SIZE = 16;
    const int TEXTURE_SHEET_WIDTH = 8;

    const int CHUNK_LAST_INDEX = VoxelWorld.CHUNK_SIZE - 1;

    const float TEXTURE_TILE_SIZE = 1.0f / TEXTURE_SHEET_WIDTH;

    static readonly Vector3I[] DIRECTIONS = VoxelWorld.DIRECTIONS;

    public VexelDict data = new VexelDict();

    public Vector3I chunkPosition = new Vector3I();
    public bool isInitialMeshGenerated = false;
    private VoxelWorld voxelWorld;

    public Thread thread;
    private Node settings;

    public override void _Ready()
    {
        voxelWorld = GetParent<VoxelWorld>();
        settings = GetNode<Node>("/root/Settings");

        Position = (Vector3)(chunkPosition * CHUNK_SIZE);
        var name = chunkPosition.ToString();
        if ((int)settings.Get("world_type") == 0)
        {
            data = TerrainGenerator.RandomBlocks();
        }
        else
        {
            data = TerrainGenerator.Flat(chunkPosition);
        }
        GenerateChunkCollider();
    }

    public void TryInitialGenerateMesh(Dictionary<Godot.Vector3I, Chunk> allChunks)
    {
        foreach (Vector3I dir in DIRECTIONS)
        {
            if (!allChunks.ContainsKey(chunkPosition + dir))
                return;
        }
        isInitialMeshGenerated = true;
        thread = new Thread(GenerateChunkMesh);
        thread.Start();
        //GenerateChunkMesh();
    }

    public void Regenerate()
    {
        foreach (Node c in GetChildren())
        {
            RemoveChild(c);
            c.QueueFree();
        }
        GenerateChunkCollider();
        GenerateChunkMesh();
    }

    void GenerateChunkCollider()
    {
        if (data.Count() == 0)
        {
            CreateBlockCollider(Vector3.Zero);
            CollisionLayer = 0;
            CollisionMask = 0;
            return;
        }

        CollisionLayer = 0xFFFFF;
        CollisionMask = 0xFFFFF;
        foreach (Vector3I blockPosition in data.Keys)
        {
            var blockId = data[blockPosition];
            if(blockId != 27 && blockId != 28)
            {
                CreateBlockCollider(blockPosition);
            }
        }
    }

    void GenerateChunkMesh()
    {
        if (data.Count() == 0)
        {
            return;
        }
        var surface_tool = new SurfaceTool();
        surface_tool.Begin(Mesh.PrimitiveType.Triangles);

        foreach (Vector3I blockPosition in data.Keys)
        {
            var block_id = data[blockPosition];
            DrawBlockMesh(surface_tool, blockPosition, block_id);
        }
        surface_tool.GenerateTangents();
        surface_tool.Index();
        ArrayMesh array_mesh = surface_tool.Commit();
        var mi = new MeshInstance3D();
        mi.Mesh = array_mesh;
        mi.MaterialOverride = GD.Load<Material>("res://world/textures/material.tres");
        //AddChild(mi);
        CallDeferred("add_child", mi);
    }

    void DrawBlockMesh(SurfaceTool surface_tool, Vector3I block_sub_position, uint block_id)
    {
        Array<Vector3> verts = Chunk.CalculateBlockVerts(block_sub_position);
        Array<Vector2> uvs = Chunk.CalculateBlockUvs(block_id);
        Array<Vector2> top_uvs = uvs;
        Array<Vector2> bottom_uvs = uvs;

        // Bush blocks get drawn in their own special way.
        if (block_id == 27 || block_id == 28)
        {
            DrawBlockFace(surface_tool, [verts[2], verts[0], verts[7], verts[5]], uvs, new Vector3(-1, 0, 1).Normalized());
            DrawBlockFace(surface_tool, [verts[7], verts[5], verts[2], verts[0]], uvs, new Vector3(1, 0, -1).Normalized());
            DrawBlockFace(surface_tool, [verts[3], verts[1], verts[6], verts[4]], uvs, new Vector3(1, 0, 1).Normalized());
            DrawBlockFace(surface_tool, [verts[6], verts[4], verts[3], verts[1]], uvs, new Vector3(-1, 0, -1).Normalized());
            return;
        }

        // Allow some blocks to have different top/bottom textures.
        if (block_id == 3) // Grass.
        {
            top_uvs = Chunk.CalculateBlockUvs(0);
            bottom_uvs = Chunk.CalculateBlockUvs(2);
        }
        else if (block_id == 5) // Furnace.
        {
            top_uvs = Chunk.CalculateBlockUvs(31);
            bottom_uvs = top_uvs;
        }
        else if (block_id == 12) // Log.
        {
            top_uvs = Chunk.CalculateBlockUvs(30);
            bottom_uvs = top_uvs;
        }
        else if (block_id == 19) // Bookshelf.
        {
            top_uvs = Chunk.CalculateBlockUvs(4);
            bottom_uvs = top_uvs;
        }

        // Main rendering code for normal blocks.
        uint other_block_id = 0;

        // Check left face
        if (block_sub_position.X == 0)
        {
            Vector3I other_sub_pos = new Vector3I(15, block_sub_position.Y, block_sub_position.Z);
            other_block_id = voxelWorld.GetBlockInChunk(chunkPosition + Vector3I.Left, other_sub_pos);
        }
        else
        {
            Vector3I other_block_sub_pos = block_sub_position + Vector3I.Left;
            if (data.ContainsKey(other_block_sub_pos))
                other_block_id = data[other_block_sub_pos];
        }
        if (block_id != other_block_id && Chunk.isBlockTransparent(other_block_id))
            DrawBlockFace(surface_tool, [verts[2], verts[0], verts[3], verts[1]], uvs, Vector3.Left);

        // Check right face
        other_block_id = 0;
        if (block_sub_position.X == CHUNK_SIZE - 1)
        {
            Vector3I other_sub_pos = new Vector3I(0, block_sub_position.Y, block_sub_position.Z);
            other_block_id = voxelWorld.GetBlockInChunk(chunkPosition + Vector3I.Right, other_sub_pos);
        }
        else
        {
            Vector3I other_block_sub_pos = block_sub_position + Vector3I.Right;
            if (data.ContainsKey(other_block_sub_pos))
                other_block_id = data[other_block_sub_pos];
        }
        if (block_id != other_block_id && Chunk.isBlockTransparent(other_block_id))
            DrawBlockFace(surface_tool, [verts[7], verts[5], verts[6], verts[4]], uvs, Vector3.Right);

        // Check forward face
        other_block_id = 0;
        if (block_sub_position.Z == 0)
        {
            Vector3I other_sub_pos = new Vector3I(block_sub_position.X, block_sub_position.Y, CHUNK_SIZE - 1);
            other_block_id = voxelWorld.GetBlockInChunk(chunkPosition + Vector3I.Forward, other_sub_pos);
        }
        else
        {
            Vector3I other_block_sub_pos = block_sub_position + Vector3I.Forward;
            if (data.ContainsKey(other_block_sub_pos))
                other_block_id = data[other_block_sub_pos];
        }
        if (block_id != other_block_id && Chunk.isBlockTransparent(other_block_id))
            DrawBlockFace(surface_tool, [verts[6], verts[4], verts[2], verts[0]], uvs, Vector3.Forward);

        // Check back face
        other_block_id = 0;
        if (block_sub_position.Z == CHUNK_SIZE - 1)
        {
            Vector3I other_sub_pos = new Vector3I(block_sub_position.X, block_sub_position.Y, 0);
            other_block_id = voxelWorld.GetBlockInChunk(chunkPosition + Vector3I.Back, other_sub_pos);
        }
        else
        {
            Vector3I other_block_sub_pos = block_sub_position + Vector3I.Back;
            if (data.ContainsKey(other_block_sub_pos))
                other_block_id = data[other_block_sub_pos];
        }
        if (block_id != other_block_id && Chunk.isBlockTransparent(other_block_id))
            DrawBlockFace(surface_tool, [verts[3], verts[1], verts[7], verts[5]], uvs, Vector3.Back);

        // Check bottom face
        other_block_id = 0;
        if (block_sub_position.Y == 0)
        {
            Vector3I other_sub_pos = new Vector3I(block_sub_position.X, CHUNK_SIZE - 1, block_sub_position.Z);
            other_block_id = voxelWorld.GetBlockInChunk(chunkPosition + Vector3I.Down, other_sub_pos);
        }
        else
        {
            Vector3I other_block_sub_pos = block_sub_position + Vector3I.Down;
            if (data.ContainsKey(other_block_sub_pos))
                other_block_id = data[other_block_sub_pos];
        }
        if (block_id != other_block_id && Chunk.isBlockTransparent(other_block_id))
            DrawBlockFace(surface_tool, [verts[4], verts[5], verts[0], verts[1]], bottom_uvs, Vector3.Down);

        // Check top face
        other_block_id = 0;
        if (block_sub_position.Y == CHUNK_SIZE - 1)
        {
            Vector3I other_sub_pos = new Vector3I(block_sub_position.X, 0, block_sub_position.Z);
            other_block_id = voxelWorld.GetBlockInChunk(chunkPosition + Vector3I.Up, other_sub_pos);
        }
        else
        {
            Vector3I other_block_sub_pos = block_sub_position + Vector3I.Up;
            if (data.ContainsKey(other_block_sub_pos))
                other_block_id = data[other_block_sub_pos];
        }
        if (block_id != other_block_id && Chunk.isBlockTransparent(other_block_id))
            DrawBlockFace(surface_tool, [verts[2], verts[3], verts[6], verts[7]], top_uvs, Vector3.Up);
    }

    void DrawBlockFace(SurfaceTool surface_tool, Array<Vector3> verts, Array<Vector2> uvs, Vector3 normal)
    {
        surface_tool.SetNormal(normal);
        surface_tool.SetUV(uvs[1]); surface_tool.AddVertex(verts[1]);
        surface_tool.SetUV(uvs[2]); surface_tool.AddVertex(verts[2]);
        surface_tool.SetUV(uvs[3]); surface_tool.AddVertex(verts[3]);

        surface_tool.SetUV(uvs[2]); surface_tool.AddVertex(verts[2]);
        surface_tool.SetUV(uvs[1]); surface_tool.AddVertex(verts[1]);
        surface_tool.SetUV(uvs[0]); surface_tool.AddVertex(verts[0]);
    }

    void CreateBlockCollider(Vector3 block_sub_position)
    {
        var collider = new CollisionShape3D();
        collider.Shape = new BoxShape3D();
        //collider.shape.extents = Vector3.ONE / 2
        ((BoxShape3D)collider.Shape).Size = Vector3.One;
        collider.Position = block_sub_position + Vector3.One / 2;
        AddChild(collider);
    }

    public static Array<Vector2> CalculateBlockUvs(uint block_id)
    {
        uint row = block_id / TEXTURE_SHEET_WIDTH;
        uint col = block_id % TEXTURE_SHEET_WIDTH;

        return [
		// Godot 4 has a weird bug where there are seams at the edge
		// of the textures. Adding a margin of 0.01 "fixes" it.
            TEXTURE_TILE_SIZE * (new Vector2(col + 0.01f, row + 0.01f)),
            TEXTURE_TILE_SIZE * new Vector2(col + 0.01f, row + 0.99f),
            TEXTURE_TILE_SIZE * new Vector2(col + 0.99f, row + 0.01f),
            TEXTURE_TILE_SIZE * new Vector2(col + 0.99f, row + 0.99f),
        ];
    }

    static Array<Vector3> CalculateBlockVerts(Vector3 block_position)
    {
        return [
            new Vector3(block_position.X, block_position.Y, block_position.Z),
            new Vector3(block_position.X, block_position.Y, block_position.Z + 1),
            new Vector3(block_position.X, block_position.Y + 1, block_position.Z),
            new Vector3(block_position.X, block_position.Y + 1, block_position.Z + 1),
            new Vector3(block_position.X + 1, block_position.Y, block_position.Z),
            new Vector3(block_position.X + 1, block_position.Y, block_position.Z + 1),
            new Vector3(block_position.X + 1, block_position.Y + 1, block_position.Z),
            new Vector3(block_position.X + 1, block_position.Y + 1, block_position.Z + 1),
        ];
    }

    static public bool isBlockTransparent(uint block_id)
    {
        return block_id == 0 || (block_id > 25 && block_id < 30);
    }
}
